package de.mlp_distributed.mlp.math.jblas;

import java.util.Iterator;
import java.util.Map;

import org.jblas.DoubleMatrix;

import com.google.common.collect.AbstractIterator;
import com.google.common.collect.Maps;

import de.mlp_distributed.mlp.math.function.DoubleDoubleFunction;
import de.mlp_distributed.mlp.math.function.DoubleFunction;
import de.mlp_distributed.mlp.math.function.Functions;
import de.mlp_distributed.mlp.math.function.VectorFunction;
import de.mlp_distributed.mlp.math.mahout.CardinalityException;
import de.mlp_distributed.mlp.math.mahout.DenseMatrix;
import de.mlp_distributed.mlp.math.mahout.IndexException;
import de.mlp_distributed.mlp.math.mahout.Matrix;
import de.mlp_distributed.mlp.math.mahout.MatrixSlice;
import de.mlp_distributed.mlp.math.mahout.MatrixVectorView;
import de.mlp_distributed.mlp.math.mahout.Vector;

public class JDenseMatrix implements Matrix {

	protected Map<String, Integer> columnLabelBindings;
	protected Map<String, Integer> rowLabelBindings;

	protected DoubleMatrix jdm;

	public JDenseMatrix() {
		super();
	}

	public JDenseMatrix(final double[][] values) {
		this(new DoubleMatrix(values));
	}

	public JDenseMatrix(final DoubleMatrix x) {
		this.jdm = x;
	}

	public JDenseMatrix(final int rows, final int columns) {
		this(new DoubleMatrix(rows, columns));
	}

	@Override
	public Matrix clone() {
		final JDenseMatrix clone = new JDenseMatrix(this.jdm.rows, this.jdm.columns);
		for (int row = 0; row < this.jdm.rows; row++) {
			for (int column = 0; column < this.jdm.columns; column++) {
				clone.setQuick(row, column, this.jdm.get(row, column));
			}
		}

		if (this.rowLabelBindings != null) {
			clone.rowLabelBindings = Maps.newHashMap(this.rowLabelBindings);
		}
		if (this.columnLabelBindings != null) {
			clone.columnLabelBindings = Maps.newHashMap(this.columnLabelBindings);
		}
		return clone;
	}

	@Override
	public Iterator<MatrixSlice> iterator() {
		return this.iterateAll();
	}

	@Override
	public Iterator<MatrixSlice> iterateAll() {
		return new AbstractIterator<MatrixSlice>() {
			private int slice;

			@Override
			protected MatrixSlice computeNext() {
				if (this.slice >= JDenseMatrix.this.numSlices()) {
					return this.endOfData();
				}
				final int i = this.slice++;
				return new MatrixSlice(JDenseMatrix.this.viewRow(i), i);
			}
		};
	}

	@Override
	public int numSlices() {
		return this.numRows();
	}

	@Override
	public int numRows() {
		return this.jdm.getRows();
	}

	@Override
	public int numCols() {
		return this.jdm.getColumns();
	}

	@Override
	public Vector times(final Vector v) {
		if (!(v instanceof JDenseVector)) {
			System.err.println(("Only instances of JDenseVector are supported!"));
			return null;
		}
		final int columns = this.columnSize();
		if (columns != v.size()) {
			throw new CardinalityException(columns, v.size());
		}

		final DoubleMatrix tmp = ((JDenseVector) v).jdm;

		final DoubleMatrix result = this.jdm.mmul(tmp);
		return new JDenseVector(result);
	}

	@Override
	public Vector timesSquared(final Vector v) {
		// TODO Auto-generated method stub
		throw new IllegalStateException("Unbound label");
	}

	@Override
	public String asFormatString() {
		// TODO Auto-generated method stub
		throw new IllegalStateException("Unbound label");
	}

	@Override
	public Matrix assign(final double value) {
		// TODO Auto-generated method stub
		throw new IllegalStateException("Unbound label");
	}

	@Override
	public Matrix assign(final double[][] values) {
		// TODO Auto-generated method stub
		throw new IllegalStateException("Unbound label");
	}

	@Override
	public Matrix assign(final Matrix other) {
		// TODO Auto-generated method stub
		throw new IllegalStateException("Unbound label");
	}

	@Override
	public Matrix assign(final DoubleFunction function) {
		// TODO Auto-generated method stub
		throw new IllegalStateException("Unbound label");
	}

	@Override
	public Matrix assign(final Matrix other, final DoubleDoubleFunction function) {
		final int rows = this.rowSize();
		if (rows != other.rowSize()) {
			throw new CardinalityException(rows, other.rowSize());
		}
		final int columns = this.columnSize();
		if (columns != other.columnSize()) {
			throw new CardinalityException(columns, other.columnSize());
		}
		for (int row = 0; row < rows; row++) {
			for (int col = 0; col < columns; col++) {
				this.setQuick(row, col, function.apply(this.getQuick(row, col), other.getQuick(row, col)));
			}
		}
		return this;
	}

	@Override
	public Matrix assignColumn(final int column, final Vector other) {
		// TODO Auto-generated method stub
		throw new IllegalStateException("Unbound label");
	}

	@Override
	public Matrix assignRow(final int row, final Vector other) {
		if (this.columnSize() != other.size()) {
			throw new CardinalityException(this.columnSize(), other.size());
		}
		if ((row < 0) || (row >= this.rowSize())) {
			throw new IndexException(row, this.rowSize());
		}
		for (int col = 0; col < this.columnSize(); col++) {
			this.set(row, col, other.getQuick(col));
		}
		return this;
	}

	@Override
	public Vector aggregateRows(final VectorFunction f) {
		final Vector r = new JDenseVector(this.numRows());
		final int n = this.numRows();
		for (int row = 0; row < n; row++) {
			r.set(row, f.apply(this.viewRow(row)));
		}
		return r;
	}

	@Override
	public Vector aggregateColumns(final VectorFunction f) {
		// TODO Auto-generated method stub
		throw new IllegalStateException("Unbound label");
	}

	@Override
	public double aggregate(final DoubleDoubleFunction combiner, final DoubleFunction mapper) {
		return this.aggregateRows(new VectorFunction() {
			@Override
			public double apply(final Vector v) {
				return v.aggregate(combiner, mapper);
			}
		}).aggregate(combiner, Functions.IDENTITY);
	}

	@Override
	public int columnSize() {
		// TODO Auto-generated method stub
		return this.jdm.columns;
	}

	@Override
	public int rowSize() {
		// TODO Auto-generated method stub
		return this.jdm.rows;
	}

	@Override
	public double determinant() {
		final int rows = this.rowSize();
		final int columns = this.columnSize();
		if (rows != columns) {
			throw new CardinalityException(rows, columns);
		}

		if (rows == 2) {
			return (this.getQuick(0, 0) * this.getQuick(1, 1)) - (this.getQuick(0, 1) * this.getQuick(1, 0));
		} else {
			// TODO: this really should just be one line:
			// TODO: new
			// CholeskyDecomposition(this).getL().viewDiagonal().aggregate(Functions.TIMES)
			int sign = 1;
			double ret = 0;

			for (int i = 0; i < columns; i++) {
				final Matrix minor = new DenseMatrix(rows - 1, columns - 1);
				for (int j = 1; j < rows; j++) {
					boolean flag = false; /* column offset flag */
					for (int k = 0; k < columns; k++) {
						if (k == i) {
							flag = true;
							continue;
						}
						minor.set(j - 1, flag ? k - 1 : k, this.getQuick(j, k));
					}
				}
				ret += this.getQuick(0, i) * sign * minor.determinant();
				sign *= -1;

			}

			return ret;
		}
	}

	@Override
	public Matrix divide(final double x) {
		return new JDenseMatrix(this.jdm.div(x));
	}

	@Override
	public double get(final int row, final int column) {
		return this.jdm.get(row, column);
	}

	@Override
	public double getQuick(final int row, final int column) {
		return this.get(row, column);
	}

	@Override
	public Matrix like() {
		return this.like(this.jdm.rows, this.jdm.columns);
	}

	@Override
	public Matrix like(final int rows, final int columns) {
		return new JDenseMatrix(rows, columns);
	}

	@Override
	public Matrix minus(final Matrix x) {
		if (!(x instanceof JDenseMatrix)) {
			return null;
		}

		return new JDenseMatrix(this.jdm.sub(((JDenseMatrix) x).jdm));
	}

	@Override
	public Matrix plus(final double x) {
		return new JDenseMatrix(this.jdm.add(x));
	}

	@Override
	public Matrix plus(final Matrix x) {
		if (!(x instanceof JDenseMatrix)) {
			return null;
		}

		return new JDenseMatrix(this.jdm.add(((JDenseMatrix) x).jdm));
	}

	@Override
	public void set(final int row, final int column, final double value) {
		this.jdm.put(row, column, value);
	}

	@Override
	public void set(final int row, final double[] data) {
		this.jdm.putRow(row, new DoubleMatrix(data));
	}

	@Override
	public void setQuick(final int row, final int column, final double value) {
		this.set(row, column, value);
	}

	@Override
	public int[] getNumNondefaultElements() {
		// TODO Auto-generated method stub
		throw new IllegalStateException("Unbound label");
	}

	@Override
	public Matrix times(final double x) {
		return new JDenseMatrix(this.jdm.mul(x));
	}

	@Override
	public Matrix times(final Matrix other) {
		final int columns = this.columnSize();
		if (columns != other.rowSize()) {
			throw new CardinalityException(columns, other.rowSize());
		}
		final int rows = this.rowSize();
		final int otherColumns = other.columnSize();
		final Matrix result = this.like(rows, otherColumns);
		for (int row = 0; row < rows; row++) {
			for (int col = 0; col < otherColumns; col++) {
				double sum = 0.0;
				for (int k = 0; k < columns; k++) {
					sum += this.getQuick(row, k) * other.getQuick(k, col);
				}
				result.setQuick(row, col, sum);
			}
		}
		return result;
	}

	@Override
	public Matrix transpose() {
		return new JDenseMatrix(this.jdm.transpose());
	}

	@Override
	public double zSum() {
		// TODO Auto-generated method stub
		throw new IllegalStateException("Unbound label");
	}

	@Override
	public Map<String, Integer> getColumnLabelBindings() {
		return this.columnLabelBindings;
	}

	@Override
	public Map<String, Integer> getRowLabelBindings() {
		return this.rowLabelBindings;
	}

	@Override
	public void setColumnLabelBindings(final Map<String, Integer> bindings) {
		this.columnLabelBindings = bindings;
	}

	@Override
	public void setRowLabelBindings(final Map<String, Integer> bindings) {
		this.rowLabelBindings = bindings;
	}

	@Override
	public double get(final String rowLabel, final String columnLabel) {
		if ((this.columnLabelBindings == null) || (this.rowLabelBindings == null)) {
			throw new IllegalStateException("Unbound label");
		}
		final Integer row = this.rowLabelBindings.get(rowLabel);
		final Integer col = this.columnLabelBindings.get(columnLabel);
		if ((row == null) || (col == null)) {
			throw new IllegalStateException("Unbound label");
		}

		return this.get(row, col);
	}

	@Override
	public void set(final String rowLabel, final String columnLabel, final double value) {
		if ((this.columnLabelBindings == null) || (this.rowLabelBindings == null)) {
			throw new IllegalStateException("Unbound label");
		}
		final Integer row = this.rowLabelBindings.get(rowLabel);
		final Integer col = this.columnLabelBindings.get(columnLabel);
		if ((row == null) || (col == null)) {
			throw new IllegalStateException("Unbound label");
		}
		this.set(row, col, value);
	}

	@Override
	public void set(final String rowLabel, final String columnLabel, final int row, final int column, final double value) {
		if (this.rowLabelBindings == null) {
			this.rowLabelBindings = Maps.newHashMap();
		}
		this.rowLabelBindings.put(rowLabel, row);
		if (this.columnLabelBindings == null) {
			this.columnLabelBindings = Maps.newHashMap();
		}
		this.columnLabelBindings.put(columnLabel, column);

		this.set(row, column, value);
	}

	@Override
	public void set(final String rowLabel, final double[] rowData) {
		if (this.columnLabelBindings == null) {
			throw new IllegalStateException("Unbound label");
		}
		final Integer row = this.rowLabelBindings.get(rowLabel);
		if (row == null) {
			throw new IllegalStateException("Unbound label");
		}
		this.set(row, rowData);
	}

	@Override
	public void set(final String rowLabel, final int row, final double[] rowData) {
		if (this.rowLabelBindings == null) {
			this.rowLabelBindings = Maps.newHashMap();
		}
		this.rowLabelBindings.put(rowLabel, row);
		this.set(row, rowData);
	}

	@Override
	public Matrix viewPart(final int[] offset, final int[] size) {
		// TODO Auto-generated method stub
		throw new IllegalStateException("Unbound label");
	}

	@Override
	public Matrix viewPart(final int rowOffset, final int rowsRequested, final int columnOffset, final int columnsRequested) {
		// TODO Auto-generated method stub
		throw new IllegalStateException("Unbound label");
	}

	@Override
	public Vector viewRow(final int row) {
		return new MatrixVectorView(this, row, 0, 0, 1);
	}

	@Override
	public Vector viewColumn(final int column) {
		// TODO Auto-generated method stub
		throw new IllegalStateException("Unbound label");
	}

	@Override
	public Vector viewDiagonal() {
		// TODO Auto-generated method stub
		throw new IllegalStateException("Unbound label");
	}
}
